contents

Java Stream (스트림) 가이드

Java 8부터 도입된 Stream API는 컬렉션, 배열, 파일 등 연속된 데이터를 간결하고 선언형으로 처리할 수 있도록 해주는 기능입니다.

1. Stream이란?

2. Stream의 주요 특징

3. 스트림 파이프라인의 구조

List<String> result = names.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList()); // 결과 얻기 (리스트)

4. Stream 생성 방법

5. 주요 스트림 연산

A. 중간 연산 (Intermediate operations)

연산 설명
filter() 조건에 맞는 요소만 필터링
map() 각 요소를 변환하거나 수정
flatMap() 중첩된 요소 펼치기 (List) → List
sorted() 정렬
distinct() 중복 제거
peek() 중간 디버깅 또는 로깅
limit(), skip() 요소 수 제한 또는 스킵

B. 최종 연산 (Terminal operations)

연산 설명
forEach() 각 요소마다 수행
collect() 결과물 수집 (List, Set 등)
reduce() 누적 집계 (합계, 곱 등)
count() 요소 수 카운트
anyMatch() 하나라도 조건 만족하는지 검사
allMatch() 모두 조건 만족 여부 검사
findFirst() 첫 번째 요소 반환 (Optional)
min()/max() Comparator 기준 최소/최대 찾기

6. 기본형 스트림 (IntStream, LongStream, DoubleStream)

IntStream.range(1, 10); // 1부터 9까지
LongStream.of(10L, 20L, 30L);

→ 박싱 비용 없이 효율적인 스트림 제공

7. 실전 예제 및 자주 쓰는 패턴

짝수 제곱값 리스트로 얻기

List<Integer> result = numbers.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

중첩 리스트 펼치기

List<String> all = listOfLists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());

그룹핑과 집계

Map<String, List<User>> byCity = users.stream()
    .collect(Collectors.groupingBy(User::getCity));

8. Parallel Stream (병렬 처리)

List<Integer> bigList = ...;
bigList.parallelStream()
    .filter(...)
    .map(...)
    .collect(Collectors.toList());

9. for-loop vs Stream 비교

특징 for-loop Stream
스타일 명령형 함수형, 선언형
병렬 처리 수동 구현 필요 .parallelStream()으로 자동 병렬 처리
가독성 루프가 길어질수록 가독성 낮아짐 간결하고 체이닝 가능
성능 단순 반복에는 빠름 복잡한 변환·조합에 유리

10. 주의할 점

11. 시각적 파이프라인 구성

[데이터 소스] → [filter] → [map] → [collect/forEach]

예시:

numbers.stream()
    .filter(n -> n > 10)
    .map(n -> n * 2)
    .collect(Collectors.toList());

12. 실전 팁 & 모범 사례

✅ 결론 요약


✅ Java Stream 주요 사용 예제

Java의 Stream API를 활용한 가장 일반적인 코드 예제를 보여드리겠습니다.

1. 📌 필터링, 변환(map), 결과 수집(collect)

짝수만 필터링하고 제곱한 후 리스트로 수집

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

List<Integer> evenSquares = numbers.stream()
    .filter(n -> n % 2 == 0)        // 짝수만 통과
    .map(n -> n * n)                // 제곱값 변환
    .collect(Collectors.toList());  // 결과를 리스트로 저장
// 결과: [4, 16, 36]

2. 🏙️ 그룹핑 및 카운트

사용자를 도시별로 그룹화하고 수를 센다

class User { String city; String name; /* getter 생략 */ }
List<User> users = List.of(
    new User("Seoul", "Kim"), 
    new User("Busan", "Lee"), 
    new User("Seoul", "Park")
);

Map<String, Long> usersPerCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,         // 도시 기준 그룹화
        Collectors.counting() // 그룹당 개수 카운트
    ));
// 결과: {Seoul=2, Busan=1}

3. ➕ 합계 및 곱 (Reduce)

모든 숫자의 합 또는 곱

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

int sum = numbers.stream().mapToInt(Integer::intValue).sum(); // 합계: 10
int product = numbers.stream().reduce(1, (a, b) -> a * b);     // 곱: 24

4. 🪆 중첩 리스트 평탄화 (flatMap)

List<List<String>> → List<String>

List<List<String>> listOfLists = List.of(
    List.of("a", "b"), 
    List.of("c", "d")
);

List<String> flat = listOfLists.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList()); // 결과: [a, b, c, d]

5. 🥇 최대/최소 찾기 (max/min)

가장 점수가 높은 사용자 찾기

class User { String name; int score; /* getter 생략 */ }

Optional<User> topUser = users.stream()
    .max(Comparator.comparing(User::getScore));

topUser.ifPresent(u -> System.out.println(u.getName() + " is top!"));

6. ✅ 조건 검사 - allMatch / anyMatch / noneMatch

List<Integer> values = Arrays.asList(3, 4, 5);

boolean allAboveTwo = values.stream().allMatch(v -> v > 2);  // 모두 2 이상? → true
boolean anyAboveFour = values.stream().anyMatch(v -> v > 4); // 하나 이상 4 초과? → true
boolean noneNegative = values.stream().noneMatch(v -> v < 0); // 음수 없음? → true

7. 🔁 중복 제거 및 정렬

List<Integer> raw = Arrays.asList(5, 3, 5, 4, 3, 2);

List<Integer> uniqSorted = raw.stream()
    .distinct()           // 중복 제거
    .sorted()             // 오름차순 정렬
    .collect(Collectors.toList()); // 결과: [2, 3, 4, 5]

8. 📖 페이징 처리 (Limit & Skip)

List<String> names = Arrays.asList("Kim", "Lee", "Park", "Choi", "Jung", "Yoon");

// 페이지 2 (3건씩 자를 때, 두 번째 페이지)
List<String> page2 = names.stream()
    .skip(3)      // 앞의 3개 건너뜀
    .limit(3)     // 다음 3개 선택
    .collect(Collectors.toList()); // 결과: [Choi, Jung, Yoon]

9. 🔗 문자열 합치기 (joining)

List<String> words = Arrays.asList("spring", "java", "stream");

String result = words.stream()
    .collect(Collectors.joining(", ")); // "spring, java, stream"

10. ⚡ 병렬 스트림 (Parallel Stream)

List<Double> bigList = ...;

bigList.parallelStream()
    .map(Math::sqrt)
    .forEach(System.out::println);

이 예제들은 스트림을 활용한 Java의 주요 작업 유형들을 포괄합니다:
⭐ 필터링, 매핑, 집계, 정렬, 그룹화, 페이징, 병렬 처리 등


🔧 Java Stream 성능 최적화 – 상세 설명 및 실전 예제

Java의 Stream API는 코드를 간결하고 함수형 스타일로 작성할 수 있게 해주지만, 항상 가장 빠른 방법은 아닙니다. 특히 큰 데이터나 복잡한 스트림 체이닝에서는 성능에 주의해야 합니다. 여기에는 실무에서 꼭 알아야 할 성능 튜닝 기법과 코드 예제를 정리했습니다.

1. 전통 루프 vs Stream 성능

// 일반 for문 – 빠름
int sum = 0;
for (int n : numbers) sum += n;

// Stream 사용 – 간단한 데이터셋에선 오히려 느릴 수 있음
int streamSum = numbers.stream().mapToInt(Integer::intValue).sum();

2. Primitive Stream (IntStream 등) 사용하기

박싱/언박싱 오버헤드 제거

// boxing 발생 (성능 손실)
int sum = numbers.stream().reduce(0, Integer::sum);

// 비박싱 사용 – 속도 향상
int sum2 = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();

3. 불필요한 객체 생성 피하기

스트림 내에서 매핑 시 새로운 객체를 생성하면 GC 부담 증가

// ❌ 매번 새 Wrapper 객체 생성
stream.map(val -> new Wrapper(val)).collect(...);

// ✅ 필요 없다면 identity 사용
stream.map(Function.identity()).collect(...);

4. parallelStream()은 신중히 사용할 것

double sum = bigList.parallelStream()
    .mapToDouble(Math::sqrt)
    .sum();

✔ 반드시 성능 측정 후 사용 (벤치마크, 프로파일링으로 확인)

5. 비싼 연산은 스트림 밖에서

// ❌ 매번 스트림 안에서 계산
stream.map(n -> expensive(n)).collect(...);

// ✅ 외부에서 먼저 계산 후 스트림 활용
double res = expensiveCalculation();
stream.map(n -> n * res).collect(...);

6. groupingBy 같은 Collector 활용하기

복잡한 수작업 루프 트랜지션 보다는 Collectors API 활용이 더 효율적

// ✔ 한 번의 패스로 도시별 그룹핑
Map<String, List<User>> byCity = users.stream()
    .collect(Collectors.groupingBy(User::getCity));

7. 단축 연산 사용 (short-circuiting)

anyMatch, findFirst, limit 등은 조건이 충족되면 앞당겨 종료

// 첫 매칭 발견 시 즉시 종료 (전체 순회 안 함)
boolean found = users.stream()
    .anyMatch(u -> u.getEmail().endsWith("@gmail.com"));

8. 부작용(Side effects) 피하기

9. 순서 무시할 수 있다면 unordered() 활용

Set<Integer> set = numbers.stream()
    .unordered()
    .collect(Collectors.toSet());

10. 실 데이터 기반 벤치마크 필수

✔ 요약 테이블

이유 예시
IntStream 사용 박싱/언박싱 제거 IntStream.range(1, n)
ParallelStream은 고부하에서만 스레드 오버헤드 있음 .parallelStream().map(...)
부작용 피하기 안정성 및 병렬 처리 문제 방지 .forEach()에서 외부 리스트 건드리지 않기
short-circuit 연산 사용 빠른 종료 가능 anyMatch(...), limit(...)
그룹핑 시 groupingBy 사용 성능 + 가독성 동시 확보 Collectors.groupingBy(...)

✔ 실전 예제: 고속 스트림 패턴

💡 수백만 개의 숫자 중에서 짝수 중 가장 큰 100개를 제곱하여 합산

int result = numbers.stream()
    .parallel()
    .filter(n -> n % 2 == 0)
    .distinct()
    .sorted(Comparator.reverseOrder())
    .limit(100)
    .mapToInt(n -> n * n)
    .sum();

✅ 결론 요약

references